satkit 0.16.2

Satellite Toolkit
Documentation
{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0",
   "metadata": {},
   "source": [
    "# TLE Fitting\n",
    "\n",
    "Two-Line Element sets (TLEs) are the standard format for distributing satellite orbital elements, but they degrade in accuracy over time. When higher-fidelity state vectors are available (e.g., from GPS or precision orbit determination), it is useful to fit a new TLE to those states.\n",
    "\n",
    "`satkit.TLE.fit_from_states` performs this fit using Levenberg-Marquardt non-linear least-squares optimization. It tunes the TLE orbital parameters to minimize the difference between the input state positions and the SGP4-predicted positions. Input states are assumed to be in the GCRF frame and are internally rotated to the TEME frame used by SGP4."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Imports\n",
    "import satkit as sk\n",
    "import numpy as np\n",
    "import math as m\n",
    "\n",
    "# Create a high-precision state\n",
    "# Altitude for circular orbit\n",
    "altitude = 450e3\n",
    "\n",
    "# Radius & velocity\n",
    "r0 = altitude + sk.consts.earth_radius\n",
    "v0 = m.sqrt(sk.consts.mu_earth / r0)\n",
    "\n",
    "# Inclination\n",
    "inclination = 15 * m.pi / 180.0\n",
    "\n",
    "# Create the state (3D position in meters, 3D velocity in meters / second)\n",
    "state0 = np.array([r0, 0, 0, 0, v0 * m.cos(inclination), v0 * m.sin(inclination)])\n",
    "# Make up an epoch\n",
    "time0 = sk.time(2024, 3, 15, 13, 0, 0)\n",
    "\n",
    "# Propagate the state forward by a day with high-precision propagator\n",
    "res = sk.propagate(state0, time0, time0 + sk.duration(days=1.0))\n",
    "\n",
    "# Get interpolated states every 10 minutes\n",
    "times = [time0 + sk.duration(minutes=i) for i in range(0, 1440, 10)]\n",
    "states = [res.interp(t) for t in times]\n",
    "\n",
    "# Fit the TLE\n",
    "(tle, fitresults) = sk.TLE.fit_from_states(states, times, time0 + sk.duration(days=0.5))  # type: ignore\n",
    "\n",
    "# Print the result\n",
    "print(tle)\n",
    "print(fitresults[\"success\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2",
   "metadata": {},
   "source": [
    "## Generate Test Data\n",
    "\n",
    "To demonstrate the fitting, we create a synthetic truth trajectory by propagating a circular orbit at 450 km altitude with satkit's high-precision propagator. We then sample position and velocity states every 10 minutes over one day, and fit a TLE to those samples."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Compute position errors (differences between TLE & state)\n",
    "\n",
    "# Get the positions from sgp4\n",
    "(pteme, vteme) = sk.sgp4(tle, times)\n",
    "# Rotate positions from TEME to GCRF frame\n",
    "pgcrf = [sk.frametransform.qteme2gcrf(t) * p for t, p in zip(times, pteme)]\n",
    "# Take difference between state vector and SGP4 positions, and compute norm\n",
    "pdiff = [p - s[0:3] for p, s in zip(pgcrf, states)]\n",
    "pdiff = np.array([np.linalg.norm(p) for p in pdiff])\n",
    "\n",
    "\n",
    "# Plot position errors\n",
    "import matplotlib.pyplot as plt\n",
    "import scienceplots  # noqa: F401\n",
    "plt.style.use([\"science\", \"no-latex\", \"../satkit.mplstyle\"])\n",
    "%config InlineBackend.figure_formats = ['svg']\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(10, 5))\n",
    "ax.plot([t.as_datetime() for t in times], pdiff, color=\"black\", linewidth=2)\n",
    "ax.set_xlabel(\"Time\")\n",
    "ax.set_ylabel(\"Position Error (m)\")\n",
    "ax.set_title(\"TLE Fitting Position Errors\")\n",
    "fig.autofmt_xdate()\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4",
   "metadata": {},
   "source": [
    "## Evaluate Fit Quality\n",
    "\n",
    "Compare the fitted TLE against the original states by propagating the TLE with SGP4, rotating from TEME to GCRF, and computing position differences. Since TLEs are a simplified analytical model, some residual error is expected even with a perfect fit."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.14.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}